﻿namespace SpikingAnalyticsLib

open System.IO
open System.Collections.Generic
open SpikingNeuronLib
open GeneralUtilities

// ===============================================================================================================
//// LinkedEventPair: a pair of linked firing events of the form ((preTime, preNeuron), (postTime, postNeuron), connectionWeight, probability)
//// where probability is normally either a link probability or a spike count ratio i.e. the proportion of profile spikes that occur in the selected window
//
//type LinkedEventPair = ((int * int) * (int * int) * float * float)

// ===============================================================================================================
// OneMillisecTickDataCollector: optionally collect membrane and synaptic weight data during a run

[<AbstractClass>]
type ISampleStore(storeName, numberOfSamples:int, numberOfRows:int, startTime:int option, interval:int option, storeIndex:List<int>, store:Map<string, List<double> []>) =

    // Create storage for a group of sampled variables
    // Outer map indexed by variable name
    // Inner array indexed by 'row' i.e. neuron index or connection index
    static member MakeStorage numberOfRows (numberOfSamples:int) (sampleNames:string list) =
        sampleNames
        |> List.map (fun name ->
            let dataStore = Array.init numberOfRows (fun index -> new List<double>(numberOfSamples))
            name, dataStore
        )
        |> Map.ofList

    // Create a place to store the sample times
    // by default sample times are sequential starting from zero
    // but not if either startTime or interval are set
    static member MakeStoreIndex (numberOfSamples:int) = new List<int>(numberOfSamples)

    abstract member StoreData: CrossbarNetwork * int * bool [] * bool[] -> unit

    member this.StoreName = storeName
    member this.SampleNames = store |> Map.toSeq |> Seq.map (fun (key, value) -> key) |> Seq.toList
    member this.NumberOfSamples = numberOfSamples
    member this.CollectedSamples = storeIndex.Count
    member this.NumberOfRows = numberOfRows
    member this.StartTime = startTime
    member this.Interval = interval
    member this.SampleTimes = storeIndex
    member this.GetData(sampleName) = store.[sampleName]
    member this.ClearStore() =
        store
        |> Map.iter (fun name (dataStore:List<double> []) ->
            for index in 0..dataStore.Length do
                dataStore.[index] <- new List<double>(numberOfSamples)
        )

type MembraneStore private (numberOfSamples, numberOfRows, startTime, interval, storeIndex:List<int>, store:Map<string, List<double> []>) =
    inherit ISampleStore("MembraneData", numberOfSamples, numberOfRows, startTime, interval, storeIndex, store)

    // store membrane data (v and u) for each neuron and each msec
    override this.StoreData(spn:CrossbarNetwork, time:int, neuronMask:bool[], connectionMask:bool[]) =
        if (
            (storeIndex.Count < numberOfSamples) &&
            ((startTime.IsNone) || (time >= startTime.Value)) &&
            ((interval.IsNone) || (time % interval.Value = 0))
            )
            then
            // store data for only those neuron indices where the corresponding 'selection' flag is true
            Seq.zip3 spn.MembranePotentials spn.MembraneRecoveryValues neuronMask
            |> Seq.filter (fun (_, _, selected) -> selected)
            |> Seq.iteri (fun index (v, u, selected) ->
                store.["V"].[index].Add(v)
                store.["U"].[index].Add(u)
            )
            storeIndex.Add(time)

    new(numberOfSamples, numberOfNeurons, startTime, interval) =
        new MembraneStore(numberOfSamples, numberOfNeurons, startTime, interval,
            (ISampleStore.MakeStoreIndex numberOfSamples), (ISampleStore.MakeStorage numberOfNeurons numberOfSamples [ "V"; "U" ]))

type ConnectionStore private (numberOfSamples, numberOfRows, startTime, interval, storeIndex:List<int>, store:Map<string, List<double> []>) =
    inherit ISampleStore("ConnectionData", numberOfSamples, numberOfRows, startTime, interval, storeIndex, store)

    // store connection data (weight and derivative) for each connection and each msec
    override this.StoreData(spn:CrossbarNetwork, time:int, neuronMask:bool[], connectionMask:bool[]) =
        if (
            (storeIndex.Count < numberOfSamples) &&
            ((startTime.IsNone) || (time >= startTime.Value)) &&
            ((interval.IsNone) || (time % interval.Value = 0))
            )
            then
            // store data for only those connection indices where the corresponding 'selection' flag is true
            Seq.zip spn.Connections connectionMask
            |> Seq.filter (fun (_, selected) -> selected)
            |> Seq.iteri (fun index (connection, selected) ->
                store.["Weight"].[index].Add(connection.Weight)
                store.["Derivative"].[index].Add(connection.Derivative)
            )
            storeIndex.Add(time)

    new(numberOfSamples, numberOfConnections, startTime, interval) =
        new ConnectionStore(numberOfSamples, numberOfConnections, startTime, interval,
            (ISampleStore.MakeStoreIndex numberOfSamples), (ISampleStore.MakeStorage numberOfConnections numberOfSamples [ "Weight"; "Derivative" ]))

type NeuronDerivativeStore private (numberOfSamples, numberOfRows, startTime, interval, storeIndex:List<int>, store:Map<string, List<double> []>) =
    inherit ISampleStore("NeuronDerivativeData", numberOfSamples, numberOfRows, startTime, interval, storeIndex, store)

    // store connection data (weight and derivative) for each connection and each msec
    override this.StoreData(spn:CrossbarNetwork, time:int, neuronMask:bool[], connectionMask:bool[]) =
        if (
            (storeIndex.Count < numberOfSamples) &&
            ((startTime.IsNone) || (time >= startTime.Value)) &&
            ((interval.IsNone) || (time % interval.Value = 0))
            )
        then

            // store neuron derivative data
            Seq.zip spn.MetaplasticityNeuronDerivative neuronMask
            |> Seq.filter (fun (_, selected) -> selected)
            |> Seq.iteri (fun index (nd, selected) ->
                store.["NeuronDerivative"].[index].Add(nd)
            )

            // store synaptic derivative data (unweighted average)
            Seq.zip spn.MetaplasticityUnweightedAverageSynapticDerivative neuronMask
            |> Seq.filter (fun (_, selected) -> selected)
            |> Seq.iteri (fun index (nd, selected) ->
                store.["SynapticDerivativeUnweighted"].[index].Add(nd)
            )

            // store synaptic derivative data (weighted)
            Seq.zip spn.MetaplasticityWeightedSynapticDerivative neuronMask
            |> Seq.filter (fun (_, selected) -> selected)
            |> Seq.iteri (fun index (nd, selected) ->
                store.["SynapticDerivativeWeighted"].[index].Add(nd)
            )

            storeIndex.Add(time)

    new(numberOfSamples, numberOfNeurons, startTime, interval) =
        new NeuronDerivativeStore(numberOfSamples, numberOfNeurons, startTime, interval,
            (ISampleStore.MakeStoreIndex numberOfSamples), (ISampleStore.MakeStorage numberOfNeurons numberOfSamples [ "NeuronDerivative"; "SynapticDerivativeUnweighted"; "SynapticDerivativeWeighted" ]))

type OneMillisecTickDataCollectorParameters =
    struct
        val SelectedNeurons:int list
        val TotalNeurons:int
        val SelectedConnections:int list
        val TotalConnections:int
        val Samplers: List<ISampleStore>

        new(selectedNeurons, totalNeurons, selectedConnections, totalConnections) =
            {
                SelectedNeurons = selectedNeurons
                TotalNeurons = totalNeurons
                SelectedConnections = selectedConnections
                TotalConnections = totalConnections
                Samplers = new List<ISampleStore>()
            }

        new(selectedNeurons, totalNeurons, selectedConnections, totalConnections, numberOfMembraneSamples, numberOfConnectionSamples, ?startTime, ?interval) =
            {
                SelectedNeurons = selectedNeurons
                TotalNeurons = totalNeurons
                SelectedConnections = selectedConnections
                TotalConnections = totalConnections
                Samplers =
                    let samplers:ISampleStore list =
                        match (numberOfMembraneSamples, numberOfConnectionSamples) with
                        | 0, 0 -> []
                        | _, 0 -> [ new MembraneStore(numberOfMembraneSamples, selectedNeurons.Length, startTime, interval); ]
                        | 0, _ -> [ new ConnectionStore(numberOfConnectionSamples, selectedConnections.Length, startTime, interval); ]
                        | _, _ ->
                        [
                            new MembraneStore(numberOfMembraneSamples, selectedNeurons.Length, startTime, interval);
                            new ConnectionStore(numberOfConnectionSamples, selectedConnections.Length, startTime, interval);
                        ]
                    new List<ISampleStore>(samplers)
            }

        member this.AddSampler(sampler:ISampleStore) =
            this.Samplers.Add(sampler)

        member this.AddMembraneSampler(numberOfSamples, ?startTime, ?interval) =
            let sampler = new MembraneStore(numberOfSamples, this.SelectedNeurons.Length, startTime, interval)
            this.AddSampler(sampler :> ISampleStore)

        member this.AddConnectionSampler(numberOfSamples, ?startTime, ?interval) =
            let sampler = new ConnectionStore(numberOfSamples, this.SelectedConnections.Length, startTime, interval)
            this.AddSampler(sampler :> ISampleStore)
    end

type OneMillisecTickDataCollector private (parameters:OneMillisecTickDataCollectorParameters, storeMap:Map<string, ISampleStore>) =

    // prepare the millisec tick handler masked by the neurons and connections selected for data capture
    let neuronMask = CreateFilter parameters.SelectedNeurons parameters.TotalNeurons
    let connectionMask = CreateFilter parameters.SelectedConnections parameters.TotalConnections

    let HandleOneMilliSecTick (spn:CrossbarNetwork) sec msec =

        let absoluteMilliseconds = (sec * 1000) + msec

        storeMap
        |> Map.iter (fun storeName store -> store.StoreData(spn, absoluteMilliseconds, neuronMask, connectionMask))

    // Get the data for the specified index in a form suitable as a plot group
    // see MillisecondResolutionDataVisualisation.ShowHiResolutionDataPlotGroups()
    let GetPlotGroup (data:(List<double> [] * List<double> []) option) neuronIndex =
        if data.IsSome then
            let a, b = data.Value
            [ Seq.toArray a.[neuronIndex], Some(Seq.toArray b.[neuronIndex]) ]
        else
            []

    // Get the specified row from an array of Lists
    // where a 'row' is the time-series data for a single neuron or connection index
    let GetRow row (data:List<'a>[]) =
        data.[row]
        |> Seq.toArray

    // Get the data for a paired set of 2D arrays as a sequence of pairs of time-series data
    let GetDataRows (data1:List<'a>[]) (data2:List<'a>[]) =
        seq {
            for row in 0..data1.Length-1 do
                yield (
                    GetRow row data1, GetRow row data2
                )
        }

    // Get a row by row sequence view of the data
    let GetRowView (data:('a * 'a)[,]) =
        // Get the specified row from the 2D array
        let Get2DRow row (data:('a * 'a)[,]) =
            data.[row..row, *]
            |> Seq.cast<'a * 'a>
            |> Seq.toArray

        seq {
            for row in 0..data.Length-1 do
                yield (
                    row, Get2DRow row data
                )
        }

    // For each row in the 2D array 'data', take only the first 'count' items
    let Select2D count (data:('a * 'a)[,]) =
        let result = Array2D.zeroCreate<'a * 'a> (data.Length) count

        GetRowView data
        |> Seq.iter (fun (rowIndex, row) ->
            row
            |> Seq.take count
            |> Seq.iteri (fun colIndex item -> result.[rowIndex, colIndex] <- item)
        )
        result

    let SaveToFile filePath data =
        use sw = File.CreateText(filePath)
        data
        |> Array2D.iteri (fun row col (v, u) ->
                let line = sprintf "%d %f %f" row v u
                sw.WriteLine(line)
            )

    new(parameters:OneMillisecTickDataCollectorParameters) =
        let storeMap =
            parameters.Samplers
            |> Seq.map (fun store -> store.StoreName, store)
            |> Map.ofSeq
        new OneMillisecTickDataCollector(parameters, storeMap)

    member this.MillisecTickHandler = HandleOneMilliSecTick

    member this.NumberOfSampledNeurons = parameters.SelectedNeurons.Length

    member this.NumberOfSampledConnections = parameters.SelectedConnections.Length

    // Get the storage respository for any data samplers
    member this.SampleStore = storeMap

    // Get the number of samples collected for the specified store
    member this.GetNumberOfSamples(storeName) =
        if storeMap.ContainsKey storeName then
            Some(storeMap.[storeName].CollectedSamples)
        else
            None

    // Get the membrane data sampler (if any)
    member this.MembraneStore =
        if storeMap.ContainsKey "MembraneData" then
            Some(storeMap.["MembraneData"])
        else
            None

    // Get the connection data sampler (if any)
    member this.ConnectionStore =
        if storeMap.ContainsKey "ConnectionData" then
            Some(storeMap.["ConnectionData"])
        else
            None

    // Get the sampling times for the membrane samples
    member this.MembraneSamplingTimes =
        let store = this.MembraneStore
        if store.IsSome then
            Some(Seq.toArray store.Value.SampleTimes)
        else
            None

    // Get the sampling times for the connection samples
    member this.ConnectionSamplingTimes =
        let store = this.ConnectionStore
        if store.IsSome then
            Some(Seq.toArray store.Value.SampleTimes)
        else
            None

    /// Get the membrane data as two arrays
    member this.MembraneData =
        let store = this.MembraneStore
        if store.IsSome then
            Some(store.Value.GetData("V"), store.Value.GetData("U"))
        else
            None

    /// Get the connection data as two arrays
    // was SynapticWeightData
    member this.ConnectionData =
        let store = this.ConnectionStore
        if store.IsSome then
            Some(store.Value.GetData("Weight"), store.Value.GetData("Derivative"))
        else
            None

    /// Get membrane data for the specified neuron in a form suitable for use as a plot group
    member this.GetMembranePlotGroup neuronIndex = 
        GetPlotGroup this.MembraneData neuronIndex

    /// Get connection data for the specified connection in a form suitable for use as a plot group
    member this.GetConnectionPlotGroup connectionIndex = 
        GetPlotGroup this.ConnectionData connectionIndex

    /// Get a row-by-row view of the membrane data
    member this.MembraneDataRowView =
        if this.MembraneData.IsSome then
            let v, u = this.MembraneData.Value
            Some(GetDataRows v u)
        else
            None

    /// Get a row-by-row view of the synaptic weight data
    // was SynapticWeightDataRowView
    member this.ConnectionDataRowView =
        if this.ConnectionData.IsSome then
            let w, d = this.ConnectionData.Value
            Some(GetDataRows w d)
        else
            None

    /// Get the membrane data as an array of tuples
    member this.MembraneDataPairs =
        let store = this.MembraneStore
        if store.IsSome then
            let v, u = store.Value.GetData("V"), store.Value.GetData("U")
            let data = Array2D.zeroCreate<double * double> (v.Length) (store.Value.CollectedSamples)

            // pair the data for each row and store in the 2D array
            for rowIndex in 0..v.Length-1 do
                Seq.zip v.[rowIndex] u.[rowIndex]
                |> Seq.iteri (fun dataIndex pair -> data.[rowIndex, dataIndex] <- pair)

            Some(data)
        else
            None

    /// Get the membrane data as an array of tuples
    member this.ConnectionDataPairs =
        let store = this.ConnectionStore
        if store.IsSome then
            let w, d = store.Value.GetData("Weight"), store.Value.GetData("Derivative")
            let data = Array2D.zeroCreate<double * double> (w.Length) (store.Value.CollectedSamples)

            // pair the data for each row and store in the 2D array
            for rowIndex in 0..w.Length-1 do
                Seq.zip w.[rowIndex] d.[rowIndex]
                |> Seq.iteri (fun dataIndex pair -> data.[rowIndex, dataIndex] <- pair)

            Some(data)
        else
            None

    /// Get a row-by-row view of the membrane data pairs
    member this.MembraneDataPairsRowView =
        if this.MembraneDataPairs.IsSome then
            let data = this.MembraneDataPairs.Value
            Some(GetRowView data)
        else
            None

    /// Get a row-by-row view of the synaptic weight data pairs
    member this.ConnectionDataPairsRowView =
        if this.ConnectionDataPairs.IsSome then
            let data= this.ConnectionDataPairs.Value
            Some(GetRowView data)
        else
            None

    /// Get the first N membrane samples
    member this.FirstNMembraneSamples(numSamples) =
        if this.MembraneDataPairs.IsSome then
            let data = this.MembraneDataPairs.Value
            Some(Select2D numSamples data)
        else
            None

    /// Get the first N synaptic weight samples
    // was FirstNSynapticWeightSamples
    member this.FirstNConnectionSamples(numSamples) =
        if this.ConnectionDataPairs.IsSome then
            let data = this.ConnectionDataPairs.Value
            Some(Select2D numSamples data)
        else
            None

    /// Print the first N membrane samples to the console for the specified neuron index
    member this.ShowFirstNMembraneSamples(numSamples, neuronIndex) =
        let data = this.FirstNMembraneSamples(numSamples)
        if data.IsSome then
            Array2D.iteri (fun nindex offset (v, u) -> if nindex = neuronIndex then printfn "%d %f %f" offset v u) data.Value

    /// Print the first N synaptic weight samples to the console for the specified connection index
    member this.ShowFirstNSynapticWeightSamples(numSamples, connectionIndex) =
        let data = this.FirstNConnectionSamples(numSamples)
        if data.IsSome then
            Array2D.iteri (fun cindex offset (w, d) -> if cindex = connectionIndex then printfn "%d %f %f" offset w d) data.Value

    /// Select membrane data from a few or all neurons
    // for all pass the value None to the 'selected' parameter
    // rowIndex is NOT a neuronIndex as it indexes into just the selected neurons
    member this.SelectMembraneGroups(selected:#seq<int> option) =
        if this.MembraneDataRowView.IsSome then
            let selectedNeurons =
                if selected.IsSome then
                    selected.Value |> Set.ofSeq
                else
                    seq { for i in 0..this.NumberOfSampledNeurons-1 -> i } |> Set.ofSeq
            let result =
                this.MembraneDataPairsRowView.Value
                |> Seq.filter (fun (rowIndex, _) -> selectedNeurons.Contains rowIndex)
                |> Seq.map (fun (nindex, data) -> data)
            Some(result)
        else
            None

    /// Select weight data from a few or all connections
    // for all pass the value None to the 'selected' parameter
    // rowIndex is NOT a connectionIndex as it indexes into just the selected connections
    member this.SelectWeightGroups(selected:#seq<int> option) =
        if this.ConnectionDataRowView.IsSome then
            let selectedConnections =
                if selected.IsSome then
                    selected.Value |> Set.ofSeq
                else
                    seq { for i in 0..this.NumberOfSampledConnections-1 -> i } |> Set.ofSeq
            let result =
                this.ConnectionDataPairsRowView.Value
                |> Seq.filter (fun (rowIndex, _) -> selectedConnections.Contains rowIndex)
                |> Seq.map (fun (connectionIndex, data) -> data)
            Some(result)
        else
            None

    /// Generate a list of spike times (milliseconds) where the membrane data exceeds zero
    // rowIndex is NOT a neuronIndex as it indexes into just the sampled neurons (not all neurons)
    member this.GetSpikeTimes(rowIndex) =
        // Get a sequential view of the data (including the indices)
        let GetSequentialView (data:List<double>[]) =
            seq {
                for i in 0..data.Length-1 do
                    for j in 0..data.[i].Count-1 do
                        yield i, j, data.[i].[j]
            }

        if this.MembraneData.IsSome then
            let vData, uData = this.MembraneData.Value
            let spikeOffsets =
                GetSequentialView vData
                |> Seq.filter (fun (index, offset, v) -> index = rowIndex)
                |> Seq.map (fun (index, offset, v) -> if v > 0.0 then Some(offset) else None)
                |> Seq.choose id
                |> Seq.toList
            Some(spikeOffsets)
        else
            None

    member this.Clear() = storeMap |> Map.iter (fun name store -> store.ClearStore())

    /// Save the membrane data to the specified path
    member this.SaveMembraneData(filePath) =
        let pairData = this.MembraneDataPairs
        if pairData.IsSome then
            SaveToFile filePath pairData.Value

    /// Save the synaptic data to the specified path
    // was SaveSynapticData
    member this.SaveConnectionData(filePath) =
        let pairData = this.ConnectionDataPairs
        if pairData.IsSome then
            SaveToFile filePath pairData.Value

// ===============================================================================================================
// OneSecondTickDataCollector: optionally collect firing data during a run

type OneSecondTickDataCollector(firingEvents:seq<int * int * EventLabel>) =
    let events = new List<int * int * EventLabel>(firingEvents)

    let HandleOneSecondTick sec frate maximal ifrate (spn:CrossbarNetwork) verbose =
        if verbose then
            printfn "%d %f %f %f" sec frate maximal ifrate

        spn.GetFiringsData(false)
        |> Seq.map (fun evt ->
                let timeStamp = (sec * 1000) + evt.Time // convert firing time this second to time since start of run
                timeStamp, evt.NeuronIndex, evt.Label
            )
        |> events.AddRange

    new() = new OneSecondTickDataCollector([])

    member this.SecondTickHandler =
        fun spn verbose sec frate maximal ifrate -> HandleOneSecondTick sec frate maximal ifrate spn verbose

    member this.FiringEvents = events

    /// Return all firing events as a sequence of (time, nindex) pairs
    member this.AllEventPairs =
        events
        |> Seq.map (fun (time, nindex, isBackgroundEvent) -> time, nindex)

            /// Return all firing events as a sequence of (time, nindex, label) triplets
    member this.AllEventTriplets =
        events
        |> Seq.cast<int * int * EventLabel>

    /// Return firing events marked as foreground events as a sequence of (time, nindex) pairs
    member this.ForegroundEventPairs =
        events
        |> Seq.filter (fun (_, _, label) -> label = EventLabel.Foreground)
        |> Seq.map (fun (time, nindex, isBackgroundEvent) -> time, nindex)

    /// Return firing events marked as foreground events as a sequence of (time, nindex, label) triplets
    member this.ForegroundEventTriplets =
        events
        |> Seq.filter (fun (_, _, label) -> label = EventLabel.Foreground)

    member this.Clear() = events.Clear()

    /// Return all firing events as a list of FiringEvent items
    member this.AllFiringEvents =
        let events =
            events
            |> Seq.map (fun (time, nindex, label) -> FiringEvent(time, nindex, label))
        new List<FiringEvent>(events)

    /// Return a new OneSecondTickDataCollector containing the selected data
    member this.SelectRange(minTime, maxTime, selectForegroundEventsOnly) =
        let data =
            if selectForegroundEventsOnly then
                events
                |> Seq.filter (fun (time, nindex, label) -> (not (label = EventLabel.Background)) && (time >= minTime && time < maxTime))
            else
                events
                |> Seq.filter (fun (time, nindex, isBackground) -> time >= minTime && time < maxTime)
        new OneSecondTickDataCollector(data)

    /// Return the mode firing time given a repeated stimulus of the specified period
    /// Returns a map of the form Map<neuronIndex, firingTime * proportion>
    /// i.e. the mode firing time and the proportion of events with that time for each neuron key
    member this.GetModeFiringTime(period) =
        this.AllEventPairs
        // generate a list of firing times for each neuron
        |> Seq.fold (fun (state:Map<_, _>) (time, nindex) ->
            let times =
                if state.ContainsKey nindex then
                    state.[nindex]
                else
                    []
            Map.add nindex (time :: times) state
        ) Map.empty
        |> Map.map (fun nindex times ->
            // fold each list of firing times into a frame of the specified period and count
            let timesArray = Array.zeroCreate period
            times
            |> Seq.iter (fun time ->
                let offset = time % period
                timesArray.[offset] <- timesArray.[offset] + 1
            )
            // find the offset with the maximum count
            let time, count =
                timesArray
                |> Seq.mapi (fun offset count -> offset, count)
                |> Seq.maxBy (fun (offset, count) -> count)

            // firing time mode, proportion of firing events with this time
            time, float count / float times.Length
        )

    /// Save firing data to the specified path
    member this.Save(filePath) =
        use sw = File.CreateText(filePath)
        events
        |> Seq.iter (fun (time, nindex, label) ->
                let line = sprintf "%d %d %s" time nindex (System.Enum.GetName(label.GetType(),  label))
                sw.WriteLine(line)
            )

// ===============================================================================================================
// A type that wraps composable pattern generators
// For creating input patterns that repeat a pattern defined by a composable function

type PatternGeneratorFactory () =

    /// Create a generator that produces a sequence of FiringEvents that form a linear pattern (ascending or descending)
    static member CreateLinearPatternGenerator(minTime, timeStep, maxTime) =

        // Create a linear sequence of FiringEvents
        // maxTime should always be larger than minTime (for both ascending and descending patterns)
        // For an ascending pattern: timeStep is positive
        // For a descending pattern: timeStep is negative
        let CreateLinearFiringPattern minTime (timeStep:int) maxTime patternBlockSize repeatIndex =
            let stepSize = System.Math.Abs(timeStep)
            let patternPeriod = (maxTime - minTime + 1)                         // temporal length of pattern
            let patternIncrements = patternPeriod / stepSize                    // the number of increments in each block (pattern instance)
            let baseTime = patternBlockSize * repeatIndex                       // the block index
            let nindexStep = 800 / patternIncrements                            // nindex step size (excitatory neurons only)
            let nindexIncrement = nindexStep - 1
            // neuron index sequence
            let nindexList =
                let steps =
                    Seq.init patternIncrements (fun index ->
                            nindexStep * (index + 1) - nindexIncrement
                        )
                    |> Seq.toList
                if timeStep < 0 then
                    // descending pattern
                    List.rev steps
                else
                    // ascending pattern
                    steps
            // time sequence
            let times =
                seq {
                    for i in minTime..stepSize..maxTime -> i
                }
            Seq.zip times nindexList
            |> Seq.map (fun (time, nindex) -> FiringEvent(baseTime + time, nindex, EventLabel.Unspecified))

        CreateLinearFiringPattern minTime timeStep maxTime

    /// Create a generator that produces a sequence of repetitions of the specified FiringEvents
    static member CreateCustomPatternGenerator(firingEvents) =

        let CreateFiringPattern (firingEvents:FiringEvent[]) patternBlockSize repeatIndex =
            let baseTime = patternBlockSize * repeatIndex
            firingEvents
            |> Seq.map (fun evt -> FiringEvent(baseTime + evt.Time, evt.NeuronIndex, EventLabel.Unspecified))

        CreateFiringPattern firingEvents

    /// Create an input pattern by repeating the pattern specified by the pattern generation function
    /// according to the specified on-off stimulation sequence
    static member CreateInputPatternFromStimulationSequence(stimulationSequence:bool array, patternBlockSize, patternGenerator:int -> int -> seq<FiringEvent>) =
        let events =
            stimulationSequence
            |> Seq.mapi (fun index stimulate ->
                    if stimulate then
                        Some(patternGenerator patternBlockSize index)
                    else
                        None
                )
            |> Seq.choose id
            |> Seq.concat
            |> Seq.toArray

        events

    /// Create an input pattern by repeating the pattern specified by the pattern generation function
    /// the specified number of times where repetitions are all grouped at the start of each second
    static member CreateInputPatternInitialClump(numberOfRepetitions, patternBlockSize, patternGenerator:int -> int -> seq<FiringEvent>) =
        let events =
            Seq.init numberOfRepetitions (fun index -> patternGenerator patternBlockSize index)
            |> Seq.concat
            |> Seq.toArray
        new Pattern(events)

    /// Create an input pattern by repeating the pattern specified by the pattern generation function
    /// the specified number of times where repetitions are evenly spaced across each second
    // Ideally 1000 / patternBlockSize / numberOfRepetitions should be an integer
    static member CreateInputPatternEvenSpacing(numberOfRepetitions, patternBlockSize, patternGenerator:int -> int -> seq<FiringEvent>) =

        let adjustedBlockSize =
            let AdjustBlockSize repetitions blockSize =
                let IsIntegral periodLength blockSize =
                    let periodFloat = periodLength / float blockSize
                    let periodInt = (int periodFloat)
                    if (periodFloat = float periodInt) || (periodInt = 0) then
                        true
                    else
                        false
                let rec loop periodLength blockSize loopCount =
                    if IsIntegral periodLength blockSize then
                        if loopCount > 0 then
                            printfn "Warning: increasing pattern block size from %d to %d for integral fit" patternBlockSize blockSize
                        blockSize
                    else
                        loop periodLength (blockSize + 1) (loopCount + 1)
                loop (1000.0 / float repetitions) blockSize 0

            AdjustBlockSize numberOfRepetitions patternBlockSize

        let stimulationSequence:bool array =
            let offSeqLength =
                let numBlocks = 1000 / adjustedBlockSize
                let usedBlocks = numberOfRepetitions
                if usedBlocks > numBlocks then
                    failwith (sprintf "%d repetitions of a pattern of length %d produces a sequence length greater than 1000 msec" numberOfRepetitions adjustedBlockSize)
                let unusedBlocks = numBlocks - usedBlocks
                unusedBlocks / usedBlocks
            seq {
                for i in 1..numberOfRepetitions do
                    yield
                        seq {
                            yield true;
                            for i in 1..offSeqLength do
                                yield false;
                        }
            }
            |> Seq.concat
            |> Seq.toArray
        PatternGeneratorFactory.CreateInputPatternFromStimulationSequence(stimulationSequence, adjustedBlockSize, patternGenerator)

// ===============================================================================================================
// PathDescriptor: describes a nullable file path without specifying the extension
// A convenience type for use with the VanillaAPI

[<AllowNullLiteral>]
type PathDescriptor private (folder:string, fileBaseName:string) =

    member this.Folder = folder

    member this.FileBaseName = fileBaseName     // the file name without the extension

    // Get the full file path with the specified extension
    member this.Path(extension) =
        Path.Combine(this.Folder, sprintf "%s.%s" this.FileBaseName extension)

    // Get the folder and filename (without extension)
    static member OptionalPath(path:PathDescriptor) =
        if path = null then
            None
        else
            Some(path.Folder, path.FileBaseName)

    // Get the full file path with the specified extension
    static member FilePath(path:PathDescriptor, extension) =
        if path = null then
            None
        else
            let filePath = Path.Combine(path.Folder, sprintf "%s.%s" path.FileBaseName extension)
            Some(filePath)

    static member Create(folder:string, fileBaseName:string) =
        if folder <> null && fileBaseName <> null then
            // ensure the extension is dropped
            let fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileBaseName)
            PathDescriptor(folder, fileNameWithoutExtension)
        else
            null

// ===============================================================================================================
// CrossbarNetworkSpecifierOptions: provides any additional data needed for creation of a CrossbarNetwork

type CrossbarNetworkSpecifierOptions (metaplasticityEnabled:bool, synapticWeightLimitMinSoft:float, synapticWeightLimitMaxSoft:float, synapticWeightLimitMaxHard:float) =

    new (synapticWeightLimitMinSoft:float, synapticWeightLimitMaxSoft:float, synapticWeightLimitMaxHard:float) =
        new CrossbarNetworkSpecifierOptions(false, synapticWeightLimitMinSoft, synapticWeightLimitMaxSoft, synapticWeightLimitMaxHard)

    new (synapticWeightLimitMaxSoft:float, synapticWeightLimitMaxHard:float) =
        new CrossbarNetworkSpecifierOptions(false, 0.0, synapticWeightLimitMaxSoft, synapticWeightLimitMaxHard)

    member this.MetaplasticityEnabled = metaplasticityEnabled

    member this.SynapticWeightLimitMinSoft = synapticWeightLimitMinSoft

    member this.SynapticWeightLimitMaxSoft = synapticWeightLimitMaxSoft

    member this.SynapticWeightLimitMaxHard = synapticWeightLimitMaxHard

// ===============================================================================================================
// CrossbarNetworkSpecifier: provides the data needed for creation of a CrossbarNetwork
// A convenience type for use with the VanillaAPI

/// Provides the data needed for CrossbarNetwork network creation
type CrossbarNetworkSpecifier private (numExcitatoryNeurons:int, numInhibitoryNeurons:int, maxDelay:int, defaultExcitatoryWeight:float, defaultInhibitoryWeight:float, connections:List<Connection> option, options:CrossbarNetworkSpecifierOptions option) =

    new (numExcitatoryNeurons:int, numInhibitoryNeurons:int, maxDelay:int, defaultExcitatoryWeight:float, defaultInhibitoryWeight:float) =
        new CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight, None, None)

    new (numExcitatoryNeurons:int, numInhibitoryNeurons:int, maxDelay:int, connections:List<Connection>) =
        new CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, 0.0, 0.0, Some(connections), None)

    new (numExcitatoryNeurons:int, numInhibitoryNeurons:int, maxDelay:int, connections:Connection list) =
        new CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, 0.0, 0.0, Some(new List<Connection>(connections)), None)

    member this.TotalNeurons = numExcitatoryNeurons + numInhibitoryNeurons

    member this.NumExcitatoryNeurons = numExcitatoryNeurons

    member this.NumInhibitoryNeurons = numInhibitoryNeurons

    member this.MaxDelay = maxDelay

    member this.DefaultExcitatoryWeight = defaultExcitatoryWeight

    member this.DefaultInhibitoryWeight = defaultInhibitoryWeight

    member this.Connections = connections

    member this.Options = options

    static member Create(networkSpecifier:CrossbarNetworkSpecifier, options:CrossbarNetworkSpecifierOptions) =
        new CrossbarNetworkSpecifier(networkSpecifier.NumExcitatoryNeurons, networkSpecifier.NumInhibitoryNeurons, networkSpecifier.MaxDelay,
            networkSpecifier.DefaultExcitatoryWeight, networkSpecifier.DefaultInhibitoryWeight, networkSpecifier.Connections, Some(options))

    /// A 1000 neuron network with no connections between neurons
    static member N1000_Unconnected_Network
        with get() =
            let numExcitatoryNeurons = 800
            let numInhibitoryNeurons = 200
            let maxDelay = 20
            let connections = new List<Connection>()    // empty list
            CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections)

    /// A network of five neurons (two inputs; three outputs) connected in a W pattern with four connections
    static member W_Network
        with get() =
            let numExcitatoryNeurons = 5
            let numInhibitoryNeurons = 0
            let maxDelay = 20
            let connections = new List<Connection>([ Connection(0, 2, 1, 8.5); Connection(0, 3, 1, 8.5); Connection(1, 3, 1, 8.5); Connection(1, 4, 1, 8.5); ])
            CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections)

    /// A network of five neurons (three inputs; two outputs) with six connections between input and output layers
    static member Switch_Network
        with get() =
            let numExcitatoryNeurons = 5
            let numInhibitoryNeurons = 0
            let maxDelay = 9
            let connections = new List<Connection>([ Connection(0, 3, 1, 6.0); Connection(0, 4, 8, 6.0); Connection(1, 3, 5, 6.0); Connection(1, 4, 5, 6.0); Connection(2, 3, 9, 6.0); Connection(2, 4, 1, 6.0) ])
            CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, connections)

    /// A grid network with 100 neurons
    static member N100Network
        with get() =
            let numExcitatoryNeurons = 80
            let numInhibitoryNeurons = 20
            let maxDelay = 20
            let defaultExcitatoryWeight = 6.0
            let defaultInhibitoryWeight = -5.0
            CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight)

    /// A grid network with 1000 neurons
    static member N1000Network
        with get() =
            let numExcitatoryNeurons = 800
            let numInhibitoryNeurons = 200
            let maxDelay = 20
            let defaultExcitatoryWeight = 3.0
            let defaultInhibitoryWeight = -2.0
            CrossbarNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, maxDelay, defaultExcitatoryWeight, defaultInhibitoryWeight)

    /// Generate a specifier for N input neurons feeding a single output neuron
    static member GetNetworkNto1Specifier(numberOfInputs, delays, weights:float list) =
        let postNeuron = numberOfInputs
        let connections =
            Seq.zip3 [ for i in 0..numberOfInputs-1 -> i ] delays weights // -> (preNeuron, delay, weight)
            |> Seq.map (fun (preNeuron, delay, weight) -> new Connection(preNeuron, postNeuron, delay, weight))
            |> Seq.toList
        new CrossbarNetworkSpecifier(numberOfInputs + 1, 0, 20, connections)

// ===============================================================================================================
// IzhikevichNetworkSpecifier: provides the data needed for Izhikevich network creation
// A convenience type for use with the VanillaAPI

/// Provides the data needed for Izhikevich network creation
type IzhikevichNetworkSpecifier(numExcitatoryNeurons, numInhibitoryNeurons, numSynapsesPerNeuron:int, maxDelay:int) =

    member this.TotalNeurons = numExcitatoryNeurons + numInhibitoryNeurons

    member this.NumExcitatoryNeurons = numExcitatoryNeurons

    member this.NumInhibitoryNeurons = numInhibitoryNeurons

    member this.NumSynapsesPerNeuron = numSynapsesPerNeuron

    member this.MaxDelay = maxDelay

// ===============================================================================================================
// MembraneDataPoint: a time-point sample of the membrane values V and U

type MembraneDataPoint =
    struct
        val V:float
        val U:float

        new(v, u) =
            {
                V = v
                U = u
            }
    end

// ===============================================================================================================
// SynapticWeightDataPoint: a time-point sample of the synaptic weight and derivative

type SynapticWeightDataPoint =
    struct
        val Derivative:float
        val Weight:float

        new(derivative, weight) =
            {
                Derivative = derivative
                Weight = weight
            }
    end

// ===============================================================================================================
// BackgroundPatternGenerator: convenience type for creating background pattern generators

type BackgroundPattern =
    | None = 0
    | OneHertz = 1
    | TwoHertz = 2
    | FiveHertz = 3
    | TenHertz = 4

// ===============================================================================================================
// InputPatternType: convenience type for creating ascending or descending coherent input patterns

type InputPatternType =
    | Ascending = 0
    | Descending = 1

//=================================================================================================
// MessageService
// A service that supports multiple modes of reporting a message (e.g. console, logging to a file etc)
// e.g.        MessageService.AddReporter(fun message priority -> if priority > 2 then printfn "%s" message)
// e.g.        MessageService.Report("Help", 3)

type MessageService private() =
    static let reporters = new List<_>()
    static member AddReporter(reporter:string -> int -> unit) =
        reporters.Add(reporter)
    static member Report(message, priority) =
        for f in reporters do
            f message priority
    static member ConsoleReporter with get() = fun message priority -> if priority > 2 then printfn "%s" message

//=================================================================================================
